-
Notifications
You must be signed in to change notification settings - Fork 321
Show read receipts #1706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Show read receipts #1706
Conversation
lib/widgets/read_receipts.dart
Outdated
color: designVariables.title, | ||
).merge(weightVariableTextStyle(context, wght: 600))), | ||
if (status == FetchStatus.success && receiptCount > 0) | ||
StyledText( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very interesting, I didn't expect to be able to do this! Is this a solution to #1285?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the issue, I think this could be a solution for it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm interesting.
This wouldn't be a satisfactory solution to all of #1285, because this appears to fire up a whole XML parser for each one of these widgets, which is going to be expensive. That's acceptable in this context, where there's only one of these at a time (and it isn't a very common part of the UI anyway). It wouldn't be OK for something that could appear in, say, message content, where you might scroll past a lot of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've written up a small TextWithLink widget which does the thing we need here more directly. Pushing that to this branch as a pair of additional commits on top:
c33ee61 text: Introduce TextWithLink widget
93e8919 (squash) Use our TextWithLink instead of package:styled_text
I think for the other cases we need for #1285, I'll be happier with further developing that widget than with what package:styled_text is doing. In particular the thing that I feel least confident about in what's missing from TextWithLink is this:
/// TODO(#1285): Integrate this with l10n so that the markup can be parsed
/// from the constant translated string, with placeholders for any variables,
/// rather than the string that results from interpolating variables.
/// That way it'll be fine to interpolate variables with arbitrary text.
but package:styled_text doesn't handle that either.
7c6c52c
to
d05ac9d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Here's comments from an initial skim.
} | ||
} | ||
|
||
class ViewReadReceiptsButton extends MessageActionSheetMenuItemButton { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An intermediate commit has:
@override void onPressed() {
// TODO: open read receipts sheet
}
So it shows the button but the button doesn't do anything. That's a buggy state, so let's avoid it.
That commit is quite small, so we can just squash it into the later commit that gives this button something to do.
lib/widgets/action_sheet.dart
Outdated
/// A plain text widget for a bottom sheet with a multiline UI string. | ||
/// | ||
/// Comes with built-in 16px horizontal padding. | ||
/// | ||
/// Figma: | ||
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3481-26993&m=dev | ||
class BottomSheetPlainText extends StatelessWidget { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name is very general. It's hard for me to tell what sort of situations this is meant for, or what it does.
If I want plain text, that's what Text
does, right? It's not clear how the bottom sheet interacts with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about the new name BottomSheetInfoText
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That does suggest more information. Can you explain a bit more in the widget's doc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated its doc.
lib/widgets/read_receipts.dart
Outdated
child: status == FetchStatus.loading | ||
? CircularProgressIndicator() | ||
: status == FetchStatus.error | ||
? BottomSheetPlainText( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This chain of ?:
would be best as a switch (status)
. That way it's clear we handle all the possible values.
lib/widgets/read_receipts.dart
Outdated
final localizations = ZulipLocalizations.of(context); | ||
final designVariables = DesignVariables.of(context); | ||
|
||
return Center( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this Center
end up doing anything in the three cases other than CircularProgressIndicator
? I suspect it doesn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also centers the plain text in the sheet, especially vertically.🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current revision, I believe this Center
only has an effect in the FetchStatus.loading
state, and otherwise it does nothing, which is confusing. (BottomSheetEmptyContentPlaceholder
takes care of positioning the "no-read-receipts" message in the error and success cases.)
I think it would be reasonable to give that new widget the job of showing a loading indicator as appropriate, and then this Center
can go away. Could give it a loading
param, requiring either that or message
but not both`, and have it show the loading spinner in the place where message would go; something like:
--- lib/widgets/action_sheet.dart
+++ lib/widgets/action_sheet.dart
@@ -182,7 +182,11 @@ class BottomSheetHeader extends StatelessWidget {
}
}
-/// A "no content here" message for when a bottom sheet has no content to show.
+/// A placeholder for when a bottom sheet has no content to show.
+///
+/// Pass [message] for a "no-content-here" message,
+/// or pass true for [loading] if the content hasn't finished loading yet,
+/// but don't pass both.
///
/// Show this below a [BottomSheetHeader] if present.
///
@@ -192,26 +196,35 @@ class BottomSheetHeader extends StatelessWidget {
// TODO(design) we don't yet have a design for this;
// it was ad-hoc and modeled on [PageBodyEmptyContentPlaceholder].
class BottomSheetEmptyContentPlaceholder extends StatelessWidget {
- const BottomSheetEmptyContentPlaceholder({super.key, required this.message});
+ const BottomSheetEmptyContentPlaceholder({
+ super.key,
+ this.message,
+ this.loading = false,
+ }) : assert(message == null || !loading);
- final String message;
+ final String? message;
+ final bool loading;
@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);
- return Padding(
- padding: EdgeInsets.fromLTRB(24, 48, 24, 16),
- child: Align(
- alignment: Alignment.topCenter,
- child: Text(
+ final child = loading
+ ? CircularProgressIndicator()
+ : Text(
textAlign: TextAlign.center,
style: TextStyle(
color: designVariables.labelSearchPrompt,
fontSize: 17,
height: 23 / 17,
).merge(weightVariableTextStyle(context, wght: 500)),
- message)));
+ message!);
+
+ return Padding(
+ padding: EdgeInsets.fromLTRB(24, 48, 24, 16),
+ child: Align(
+ alignment: Alignment.topCenter,
+ child: child));
}
}
lib/widgets/read_receipts.dart
Outdated
// TODO: deduplicate the code with [ViewReactionsUserItem] | ||
@visibleForTesting | ||
class ReadReceiptsUserItem extends StatelessWidget { | ||
const ReadReceiptsUserItem(this.pageContext, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: widget parameters should always be named (with very rare exceptions like Text
)
lib/widgets/read_receipts.dart
Outdated
padding: EdgeInsets.symmetric(vertical: 8), | ||
itemCount: userIds.length, | ||
itemBuilder: (context, index) => | ||
ReadReceiptsUserItem(context, userId: userIds[index])))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The child widget here calls this argument pageContext
, but the context we're passing looks like just its own parent's context. If this context works, then I expect the child's own context would work equally well.
(This is also an example of why it's good for widget parameters to be named — if this said pageContext: context
, then the contrast would jump out more.)
lib/widgets/read_receipts.dart
Outdated
/// https://zulip.com/api/get-read-receipts | ||
Future<GetReadReceiptsResult> getReadReceipts(ApiConnection connection, { | ||
required int messageId, | ||
}) { | ||
return connection.get('getReadReceipts', GetReadReceiptsResult.fromJson, | ||
'messages/$messageId/read_receipts', null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should go in lib/api/ :slightly_smiling_face:
lib/widgets/read_receipts.dart
Outdated
color: designVariables.title, | ||
).merge(weightVariableTextStyle(context, wght: 600))), | ||
if (status == FetchStatus.success && receiptCount > 0) | ||
StyledText( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm interesting.
This wouldn't be a satisfactory solution to all of #1285, because this appears to fire up a whole XML parser for each one of these widgets, which is going to be expensive. That's acceptable in this context, where there's only one of these at a time (and it isn't a very common part of the UI anyway). It wouldn't be OK for something that could appear in, say, message content, where you might scroll past a lot of them.
2ed033d
to
2b769fd
Compare
Thanks @gnprice for the previous review. New changes pushed. Marked for Chris's review. |
2b769fd
to
24a4e8e
Compare
Pushed a new revision with tests included. |
24a4e8e
to
961fcd8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this is exciting! Comments below, from reading all the commits except the last, which I've read only partially so far.
@@ -48,143 +48,146 @@ abstract final class ZulipIcons { | |||
/// The Zulip custom icon "check". | |||
static const IconData check = IconData(0xf108, fontFamily: "Zulip Icons"); | |||
|
|||
/// The Zulip custom icon "check_check". | |||
static const IconData check_check = IconData(0xf109, fontFamily: "Zulip Icons"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
icons: Add check_check icon, from Web Figma file
I started a discussion on this; I don't think there's a change to make right now, but please link to it in the commit message for context: #mobile-design > read-receipts icon in #F1706 @ 💬
lib/widgets/action_sheet.dart
Outdated
/// A header for a bottom sheet with a multiline UI string. | ||
/// A plain text widget for a bottom sheet with a multiline UI string. | ||
/// | ||
/// Assumes 8px padding below the top of the bottom sheet. | ||
/// Use it to present short, non-interactive explanatory messages to the user, | ||
/// such as an error message or other feedback. | ||
/// | ||
/// Comes with built-in 16px horizontal padding. | ||
/// | ||
/// Figma: | ||
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3481-26993&m=dev |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
action_sheet: Rename BottomSheetHeaderPlainText to BottomSheetInfoText
This commit decreases the number of widgets that
- declare the job of following one specific source in the Figma,
- do that job correctly, and
- have an implementation that's easy to compare to that source. 🙂
Let's make a new widget for the new not-header thing, and keep this widget for a header. This would be an example of the principle "prefer duplication over the wrong abstraction", I think 🙂, which Greg taught me about a while ago: https://chat.zulip.org/#narrow/sender/2187-Greg-Price/search/wrong.20abstraction
aside
I could more easily imagine BottomSheetHeaderPlainText
growing a "title/body" variant, if that's helpful for following this part of the Figma—

; it would still have the "header" abstraction, which seems right. In fact, that might be helpful—the Figma's 18px horizontal padding kind of looks like a mistake to me; the plain-text version has 16px. A shared widget would be a natural place for a code comment that mentions both kinds of headers. We'd want to make sure the widget still has a clear, coherent interface, with a link to the Figma source for the new behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could more easily imagine BottomSheetHeaderPlainText growing a "title/body" variant,...
Should we create a new widget for the header in this PR, with a name like BottomSheetHeader
? One distinction of this from BottomSheetHeaderPlainText
is that the latter has a different styling than the title/body of this header widget.
(Thanks for the link to that interesting blog post! 🙂)
assets/l10n/app_en.arb
Outdated
"@actionSheetReadReceipts": { | ||
"description": "Title for the \"Read receipts\" bottom sheet." | ||
}, | ||
"actionSheetReadReceiptsReadCount": "This message has been <link href=\"https://chat.zulip.org/help/read-receipts\">read</link> by {count} {count, plural, =1{person} other{people}}:", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't give translators control of the URL. If the Help Center page moves, we should only have to make a one-line change in our source code, instead of going through and changing language data for all the languages. 🙂
See web's string, can we do something like <z-link>
there?
{num_of_people, plural, one {This message has been <z-link>read</z-link> by {num_of_people} person:} other {This message has been <z-link>read</z-link> by {num_of_people} people:}}
Also I think we should link to https://zulip.com/help/read-receipts
instead of that page on CZO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also IIUC that plural syntax works, but I think we'd normally do
{count, plural, =1{1 person} other{{count} people}}
in case that's a more helpful model when translating into a language that varies the word order among the one/two/few/many/other cases? (I'm not actually sure if such languages exist actually.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, in fact best to have the whole sentence inside the plural block — like it is in that example from web.
That way if e.g. the form of the verb needs to change too, the translator has that flexibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That way if e.g. the form of the verb needs to change too, the translator has that flexibility.
nit: IIUC the translator has that flexibility already; they can choose to write the translated string however they want, including by re-scoping the plural block (or even omitting it which would probably be wrong). But by putting the whole sentence in the plural block in English, we're potentially helping by minimizing the things a translator has to think through in order to make a correct translation for their language.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. So it's more that we're giving them a model that makes it easier to see how to apply that flexibility.
lib/widgets/action_sheet.dart
Outdated
@@ -720,6 +721,7 @@ void showMessageActionSheet({required BuildContext context, required Message mes | |||
ReactionButtons(message: message, pageContext: pageContext), | |||
if (hasReactions) | |||
ViewReactionsButton(message: message, pageContext: pageContext), | |||
ViewReadReceiptsButton(message: message, pageContext: pageContext), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should store and check the org's "Enable read receipts" setting before including the button; see realm_enable_read_receipts
in https://zulip.com/api/register-queue
lib/widgets/read_receipts.dart
Outdated
overlayColor: WidgetStateColor.resolveWith((states) => | ||
states.any((e) => e == WidgetState.pressed) | ||
? designVariables.contextMenuItemBg.withFadedAlpha(0.20) | ||
: Colors.transparent), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about
overlayColor: WidgetStateColor.fromMap({
WidgetState.pressed: designVariables.contextMenuItemBg.withFadedAlpha(0.20)
}),
?
I updated my #1700 with that in response to #1700 (comment) 🙂 (there might be other things that would be helpful to bring over from ViewReactionsUserItem
in my current revision there)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here's a full review of that last commit. Generally looks great; small comments below.
lib/widgets/read_receipts.dart
Outdated
@override | ||
Widget build(BuildContext context) { | ||
return SizedBox( | ||
height: 500, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the view-reactions sheet, I used 400 with a TODO(design) tune
. I'd like to be consistent between the two places unless there's a reason not to, but I don't mind which value is used; 500 is fine if you think it's better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. ThisSizedBox
is a little bit different from the SizedBox
in view-reactions sheet. Here, the height of the view-receipts sheet is 500, and in view-reactions sheet, the height of the user-item list is 400 (excluding the reactions-header and dismiss-button). In fact, the height of that sheet is 518px (I can make this sheet's size 518 to make it consistent). I wrapped SizedBox
on the entire sheet (instead of the user-item list) so that when there are no read receipts, the sheet's height is not changed.
Maybe the same approach could be applied to view-reactions sheet to address the following comment:
zulip-flutter/lib/widgets/emoji_reaction.dart
Lines 770 to 779 in 98b94bd
// TODO if all reactions (or whole message) disappeared, | |
// we show a message saying there are no reactions, | |
// but the layout shifts (the sheet's height changes dramatically); | |
// we should avoid this. | |
if (reactionType != null && emojiCode != null) Flexible( | |
child: ViewReactionsUserList( | |
messageId: widget.messageId, | |
reactionType: reactionType!, | |
emojiCode: emojiCode!, | |
emojiName: emojiName!)), |
lib/widgets/read_receipts.dart
Outdated
@override | ||
void didChangeDependencies() { | ||
super.didChangeDependencies(); | ||
|
||
tryFetchReadReceipts(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means we can end up fetching multiple times, so I think my ideal version would have generation
logic like in lib/model/message_list.dart, to invalidate stale fetches. If results arrive out of order, we don't want to get stuck showing old results when we know we have results from a fetch we made more recently.
I think that can be a followup though—I can write up a more detailed proposal later, or perhaps just send a draft to demonstrate, once this work is merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the reasons we would want to do a new fetch because a dependency changed?
I think it may only be if there's a new store. So this could go in an onNewStore method instead.
Then the generation-like logic can just be: when getting the response, check if the store we had at the start of the request is still the current store (with identical
). If not, ignore the response.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, that's reasonable. In zulip-mobile I implemented polling to refresh this page every 15s (IIRC), and generic generation
logic would help if we did that as a followup.
But arguably the polling wasn't really needed in zulip-mobile, and it's maybe less compelling in zulip-flutter, where the view is more clearly ephemeral, not something you'd necessarily expect to stay up-to-date if you left it foregrounded in the app. (A modal bottom sheet vs. a whole full-screen page with a back button.)
}); | ||
} | ||
|
||
class ReadReceipts extends StatefulWidget { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's link to the Figma, so it's easier for readers to check the implementation against the spec.
lib/widgets/read_receipts.dart
Outdated
final designVariables = DesignVariables.of(context); | ||
|
||
return Padding( | ||
padding: EdgeInsetsDirectional.fromSTEB(18, 16, 18, 8), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a rare case where I disagree with the Figma (link). Can we change the horizontal padding to 16 and the bottom padding to 4, like in BottomSheetHeaderPlainText
(which in main
follows this in the Figma)?
Here's a before/after of my proposal (from the current revision):
Before | After |
---|---|
![]() |
![]() |
You can leave a code comment explaining that we prefer consistency with that other header, with a link to this GitHub comment.
lib/widgets/read_receipts.dart
Outdated
|
||
@override | ||
Widget build(BuildContext context) { | ||
final localizations = ZulipLocalizations.of(context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we generally write zulipLocalizations
instead of localizations
; that way it's distinct from other localizations like MaterialLocalizations
.
lib/widgets/read_receipts.dart
Outdated
style: TextStyle( | ||
decoration: TextDecoration.underline, | ||
color: designVariables.link, | ||
decorationColor: designVariables.link), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Figma specifies some other details about the underline style. Let's follow any of those that we can (e.g. decorationThickness
, I think) and write TODO(upstream)
s for the others.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's an important difference between the two thicknesses in Figma and Flutter, and I am not sure how we can map between them. In Figma, it's a percentage of the font size of the text to which the decoration belongs. In Flutter, it is a multiplier of the thickness defined by the font.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Odd, thanks for noticing that.
From Flutter's decorationThickness
doc, it does seem like you can provide a constant (like 1.5 or 2.0 or whatever) and the thickness you actually get will change with the font size. That seems desirable.
But if it's true that font files always include a preferred underline stroke-width value (as that doc says), I haven't been able to find that value for the font we're using here, Source Sans 3. 🤷♂️
lib/widgets/theme.dart
Outdated
@@ -171,6 +171,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> { | |||
labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(), | |||
labelMenuButton: const Color(0xff222222), | |||
labelSearchPrompt: const Color(0xff000000).withValues(alpha: 0.5), | |||
labelTime: const Color(0x00000000).withValues(alpha: 0.49), | |||
link: const Color(0xff066bd0), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I didn't find this in the list of Figma variables that I usually look at.
Turns out it's defined in the "Zulip Web UI kit": https://www.figma.com/design/msWyAJ8cnMHgOMPxi7BUvA/Zulip-Web-UI-kit?node-id=0-1&p=f&vars=1&var-id=b9c211a7c212568bc84139413454dd8dd07321fb%2F833-56&m=dev

It has distinct light/dark values; we should use both. And maybe add a comment like
link: const Color(0xff066bd0), // From "Zulip Web UI kit"
test/widgets/read_receipts_test.dart
Outdated
Finder findUserItem(String fullName) => find.ancestor(of: find.text(fullName), | ||
matching: find.byType(ReadReceiptsUserItem)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about:
Finder findUserItem(String fullName) => find.ancestor(of: find.text(fullName), | |
matching: find.byType(ReadReceiptsUserItem)); | |
Finder findUserItem(String fullName) => | |
find.widgetWithText(ReadReceiptsUserItem, fullName); |
?
test/widgets/read_receipts_test.dart
Outdated
delay: transitionDurationObserver.transitionDuration | ||
+ const Duration(milliseconds: 100)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It takes some nonlocal reasoning to work out which nav-transition this is about, and I notice that we're always passing the same duration here.
How about replacing prepareReceiptsResponse
with prepareReceiptsResponseSuccess(List<int> userIds)
and prepareReceiptsResponseError(Object? httpException)
, and control the delay in setupReceiptsSheet
?
test/widgets/read_receipts_test.dart
Outdated
check(find.byWidgetPredicate((widget) => switch(widget) { | ||
ProfilePage(userId: 1) => true, | ||
_ => false, | ||
})).findsOne(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be two lines instead of four I think 🙂
check(find.byWidgetPredicate((widget) => widget is ProfilePage && widget.userId == 1))
.findsOne();
lib/widgets/read_receipts.dart
Outdated
final result = await getReadReceipts(store.connection, messageId: widget.messageId); | ||
// TODO(i18n): add locale-aware sorting | ||
userIds = result.userIds.sortedByCompare( | ||
(id) => store.userDisplayName(id), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relatedly: in general we never want to keep a reference to the store across an await and use it again after, because the old store might already have been disposed. Instead, after an await we should look up the store afresh using a context.
4046f17
to
6ae29f9
Compare
Thanks @chrisbobbe for the review! New revision pushed, PTAL. |
Posted a comment a few minutes ago on a subthread above which should also be made visible here in chronological order on the PR thread. So here's a link #1706 (comment) and a copy:
|
93e8919
to
9720436
Compare
Thanks @gnprice for the new code. Changes pushed. |
bc2a507
to
9cfa559
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Comments below, and I've pushed four new commits:
f92e7f9 action_sheet [nfc]: Make BottomSheetHeaderPlainText more flexible; rename
9ee2e3c action_sheet: Implement BottomSheetEmptyContentPlaceholder, to use soon
bad5747 read_receipts [nfc]: Use BottomSheetHeader for read-receipts header
4eacb89 read_receipts: Use BottomSheetEmptyContentPlaceholder for empty-content
These are suggestions; if they make sense (see my comments), please rearrange the branch
- to put the first two of those before the main commit that implements the feature, and
- squash the last two commits into that main commit.
Excited to have this soon! 🎉
lib/widgets/text.dart
Outdated
span = TextSpan(text: match.group(1), children: [ | ||
TextSpan(text: match.group(2), recognizer: _recognizer, | ||
style: TextStyle( | ||
decoration: TextDecoration.underline, | ||
decorationStyle: TextDecorationStyle.solid, | ||
// We use the default value for this, as there's no obvious | ||
// way to map the thickness value from Figma design as it is | ||
// a percentage of the font size. | ||
decorationThickness: 1, | ||
// decorationOffset: // TODO(upstream): https://github.com/flutter/flutter/issues/30541 | ||
color: designVariables.link, | ||
decorationColor: designVariables.link)), | ||
TextSpan(text: match.group(3)), | ||
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This parent/child structure feels odd; can we make the three parts be siblings instead?
span = TextSpan(children: [
TextSpan(text: match.group(1)),
TextSpan(text: match.group(2), /* etc. */),
TextSpan(text: match.group(3)),
]);
lib/widgets/action_sheet.dart
Outdated
/// A plain text widget for informational content in a bottom sheet. | ||
/// | ||
/// Use it to present short, non-interactive explanatory messages to the user, | ||
/// such as an error message or other feedback. | ||
/// | ||
/// Comes with built-in 16px horizontal padding. | ||
/// | ||
/// Style-wise, this mostly follows the design of [BottomSheetHeaderPlainText], | ||
/// as there is no design for this in Figma right now. | ||
// TODO(design) create | ||
class BottomSheetInfoText extends StatelessWidget { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking more about this, I think my preference is:
- Declare an API that's explicitly about "empty content". One sign of a nice API is that, when you read about it, it's clear early on whether it's something you want to use or not. This
BottomSheetInfoText
widget catches the attention of a would-be caller who wants to show some "info text", but then it's not clear whether this orBottomSheetHeaderPlainText
is the better way to do that—especially because this says the style is modeled on that header widget :) - Instead of
BottomSheetInfoText
, I suggest basically duplicatingPageBodyEmptyContentPlaceholder
, adapted for use in a bottom sheet, and with bidirectional links. Then:- "Empty content placeholder" answers the "should-I-use-this" question more quickly than "info text".
- It actually sort of comes from a Figma frame, though indirectly 🙂
For example:
/// A "no content here" message for when a bottom sheet has no content to show.
///
/// Show this below a [BottomSheetHeader] if present.
///
/// See also:
/// * [PageBodyEmptyContentPlaceholder], for a similar element to use in
/// pages on the home screen.
// TODO(design) we don't yet have a design for this;
// it was ad-hoc and modeled on [PageBodyEmptyContentPlaceholder].
class BottomSheetEmptyContentPlaceholder extends StatelessWidget {
const BottomSheetEmptyContentPlaceholder({super.key, required this.message});
final String message;
@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);
return Padding(
padding: EdgeInsets.fromLTRB(24, 48, 24, 16),
child: Align(
alignment: Alignment.topCenter,
child: Text(
textAlign: TextAlign.center,
style: TextStyle(
color: designVariables.labelSearchPrompt,
fontSize: 17,
height: 23 / 17,
).merge(weightVariableTextStyle(context, wght: 500)),
message)));
}
}
Here's how it would look:
Before | After |
---|---|
![]() |
![]() |
![]() |
![]() |
I'll push a draft commit with that example.
c2b8047
to
386882f
Compare
Thanks @chrisbobbe for the review and the code. New revision pushed. Also, it would be great to have your thoughts on #1706 (comment) about making the height of view-reactions and view-read-receipts action sheets the same. 🙂 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, thanks for the ping on the height question :). I think the height and small inconsistency is fine to merge; I don't want to block this on further changes about the height, and I'm working on something locally that I might send as a followup.
Small comments below.
lib/widgets/read_receipts.dart
Outdated
final url = PerAccountStoreWidget.of(context).tryResolveUrl(ReadReceipts._helpCenterReference) | ||
?? Uri.parse(ReadReceipts._helpCenterUrl); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can resolving the URL ever actually fail? I don't think it can; we control the path part ("/help/read-receipts"), and we have the realm URL as a Uri
object, which means it passed some level of validation. I think we can do tryResolveUrl(…)!
(throwing an error if it fails), instead of opening a page on zulip.com that might have wrong information because it was written to correspond with a different Zulip server version.
lib/widgets/read_receipts.dart
Outdated
final localizations = ZulipLocalizations.of(context); | ||
final designVariables = DesignVariables.of(context); | ||
|
||
return Center( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current revision, I believe this Center
only has an effect in the FetchStatus.loading
state, and otherwise it does nothing, which is confusing. (BottomSheetEmptyContentPlaceholder
takes care of positioning the "no-read-receipts" message in the error and success cases.)
I think it would be reasonable to give that new widget the job of showing a loading indicator as appropriate, and then this Center
can go away. Could give it a loading
param, requiring either that or message
but not both`, and have it show the loading spinner in the place where message would go; something like:
--- lib/widgets/action_sheet.dart
+++ lib/widgets/action_sheet.dart
@@ -182,7 +182,11 @@ class BottomSheetHeader extends StatelessWidget {
}
}
-/// A "no content here" message for when a bottom sheet has no content to show.
+/// A placeholder for when a bottom sheet has no content to show.
+///
+/// Pass [message] for a "no-content-here" message,
+/// or pass true for [loading] if the content hasn't finished loading yet,
+/// but don't pass both.
///
/// Show this below a [BottomSheetHeader] if present.
///
@@ -192,26 +196,35 @@ class BottomSheetHeader extends StatelessWidget {
// TODO(design) we don't yet have a design for this;
// it was ad-hoc and modeled on [PageBodyEmptyContentPlaceholder].
class BottomSheetEmptyContentPlaceholder extends StatelessWidget {
- const BottomSheetEmptyContentPlaceholder({super.key, required this.message});
+ const BottomSheetEmptyContentPlaceholder({
+ super.key,
+ this.message,
+ this.loading = false,
+ }) : assert(message == null || !loading);
- final String message;
+ final String? message;
+ final bool loading;
@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);
- return Padding(
- padding: EdgeInsets.fromLTRB(24, 48, 24, 16),
- child: Align(
- alignment: Alignment.topCenter,
- child: Text(
+ final child = loading
+ ? CircularProgressIndicator()
+ : Text(
textAlign: TextAlign.center,
style: TextStyle(
color: designVariables.labelSearchPrompt,
fontSize: 17,
height: 23 / 17,
).merge(weightVariableTextStyle(context, wght: 500)),
- message)));
+ message!);
+
+ return Padding(
+ padding: EdgeInsets.fromLTRB(24, 48, 24, 16),
+ child: Align(
+ alignment: Alignment.topCenter,
+ child: child));
}
}
386882f
to
dd226c0
Compare
Thanks for the review. New changes pushed. |
dd226c0
to
db7458c
Compare
Web Figma link: https://www.figma.com/design/jbNOiBWvbtLuHaiTj4CW0G/Zulip-Web-App?node-id=7352-690&t=OXW354YOc17R7B6G-0 This icon is different from the icon in the mobile Figma design. Alya suggested to use the web version and maybe make some adjustments to it. Mobile Figma link: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=501-19868&t=bRbH05njayjM74v3-0 Related CZO discussion: https://chat.zulip.org/#narrow/channel/530-mobile-design/topic/read-receipts.20icon.20in.20.23F1706/near/2234758
db7458c
to
b4cd5aa
Compare
Thanks, LGTM! Marking for Greg's review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks to you both for building this!
Comments below. A few are more for @chrisbobbe, and others for @sm-sayedi 🙂
lib/widgets/action_sheet.dart
Outdated
|
||
final String text; | ||
/// | ||
/// Figma; title and text: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this and just above say "text" for what the code calls "subtitle".
lib/widgets/emoji_reaction.dart
Outdated
child: BottomSheetHeaderPlainText(text: zulipLocalizations.seeWhoReactedSheetNoReactions), | ||
child: BottomSheetHeader(subtitle: zulipLocalizations.seeWhoReactedSheetNoReactions), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels odd to me to have something called a "subtitle" when there's no title.
Maybe "note"? "message"?
I guess "message" might sound like it's referring to a Zulip message; but we already use that name for dialogs and it seems fine. In any case "note" doesn't have that issue.
lib/widgets/action_sheet.dart
Outdated
final baseSubtitleStyle = TextStyle( | ||
color: designVariables.labelTime, | ||
fontSize: 17, | ||
height: 22 / 17); | ||
|
||
final effectiveSubtitle = switch ((buildSubtitle, subtitle)) { | ||
(WidgetBuilderFromTextStyle build, null) => build(baseSubtitleStyle), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, interesting idea passing the desired style as an argument to the builder callback.
lib/widgets/action_sheet.dart
Outdated
(WidgetBuilderFromTextStyle build, null) => build(baseSubtitleStyle), | ||
(null, String data) => Text(style: baseSubtitleStyle, data), | ||
_ => null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can make less verbose by leaving out the types
(WidgetBuilderFromTextStyle build, null) => build(baseSubtitleStyle), | |
(null, String data) => Text(style: baseSubtitleStyle, data), | |
_ => null, | |
(final build?, null) => build(baseSubtitleStyle), | |
(null, final data?) => Text(style: baseSubtitleStyle, data), | |
_ => null, |
lib/widgets/action_sheet.dart
Outdated
// (should have been caught by `assert` in constructor) | ||
if (effectiveTitle == null && effectiveSubtitle == null) throw ArgumentError(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I don't like throwing from a build method, in release mode, if we can avoid it. Certainly not going out of our way to detect a reason to throw.
What happens if this case somehow occurs and we don't have this line? The Column will have no children — so probably it's just empty, right? That's a lot better than crashing.
OTOH it'd be fine to have an assert here. That'd affect debug mode only, the same way as the assert in the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove the assert here, as it will already be caught in the constructor anyway?
lib/widgets/read_receipts.dart
Outdated
}); | ||
} | ||
|
||
/// The read receipts sheet. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
/// The read receipts sheet. | |
/// The read-receipts sheet. |
lib/widgets/read_receipts.dart
Outdated
|
||
final int messageId; | ||
|
||
static const _helpCenterReference = '/help/read-receipts'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: "reference" is a bit vague about what sort of thing this is.
I think "relative URL" is a good term. The official term in the spec would be "relative-URL string":
https://url.spec.whatwg.org/#relative-url-string
and here the type says it's a string so we don't need to repeat that part. Therefore:
static const _helpCenterReference = '/help/read-receipts'; | |
static const _helpCenterRelativeUrl = '/help/read-receipts'; |
(In the now-obsolete RFCs that previously defined the concept of a URL, this was a "URI-reference" and more specifically a "relative reference". Perhaps that's what you had in mind with this name.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
separate nit: this private const can go on the one class that uses it, and then the reference to it doesn't need to repeat the class name.
lib/widgets/read_receipts.dart
Outdated
|
||
// TODO(i18n): add locale-aware sorting | ||
userIds = result.userIds.sortedByCompare( | ||
(id) => storeNow.userDisplayName(id), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since store
and storeNow
are identical, this is equivalent:
(id) => storeNow.userDisplayName(id), | |
store.userDisplayName, |
and keeps to our convention of always calling the store just store
.
(This version also incorporates a nit that this can use a tearoff instead of a fresh closure.)
lib/widgets/read_receipts.dart
Outdated
} | ||
|
||
|
||
// TODO: deduplicate the code with [ViewReactionsUserItem] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: double blank line
// TODO: deduplicate the code with [ViewReactionsUserItem] | ||
@visibleForTesting | ||
class ReadReceiptsUserItem extends StatelessWidget { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment should get a twin at the other end, pointing back to this one.
That way when editing the other one, we might notice and look at this one to edit it the same way (or to go ahead and do this deduplication).
Ah, thanks Greg! @sm-sayedi would you mind taking care of Greg's feedback on the code I wrote? That seems simpler process-wise than if we both try to make our own edits to the branch, and I'd appreciate it :) |
@chrisbobbe Sure, doing it now. |
Fixes part of zulip#1285. In particular this handles the case we need at the moment for zulip#667, read receipts: a sentence with a single link in the middle of it. Other cases noted in zulip#1285 are for a code font; or italics; or inserting more string fragments before and after a tagged string (for a Markdown link in quote-and-reply). There's also zulip#1553 which calls for a sentence with two different links in it (to use in an empty inbox).
…name Before, this widget supported just one plain-styled multiline UI string: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3481-26993&m=dev In addition to that feature, it now also supports: - Richer text for that string, such as TextWithLink, which we'll use for read-receipts: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=11367-20898&m=dev - A "title" line above that string, which is bigger and bolder; we'll also use that for read-receipts. - The header may appear with - a title and subtitle (as in the normal read-receipts case), or - just a title (as in read-receipts when loading or when nobody has read the message), or - just a message, as in the current caller in the view-reactions sheet - (The title may be richly styled too; we can use this when showing the channel name in the channel action sheet header, zulip#1533, which calls for showing the channel privacy icon.)
Fixes: zulip#667 Co-authored-by: Greg Price <greg@zulip.com> Co-authored-by: Chris Bobbe <cbobbe@zulip.com>
b4cd5aa
to
9f35c44
Compare
Thanks @gnprice for the review. New changes pushed, PTAL. |
Support for viewing read receipts of a message.
Figma link: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=11367-20647&t=bpmAb3Fr9aJdMIHw-0
Screenshots
Screen recording
Read.receipts.-.Screen.recording.mov
Fixes: #667